package cfengMiddleware.server.service.impl;

import cfengMiddleware.server.dto.RedPackageDto;
import cfengMiddleware.server.service.RedDetailService;
import cfengMiddleware.server.service.RedService;
import cfengMiddleware.server.util.RedPacketUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author Cfeng
 * @date 2022/9/13
 * 发红包抢红包的核心业务，需要将红包记录进入缓存中，所以需要引入Redis
 */

@Service
@Slf4j
@RequiredArgsConstructor
public class RedServiceImpl implements RedService {

    private final RedDetailService redDetailService;

    private final RedisTemplate redisTemplate;

    //使用Zookeeper作为分布式锁
    private final CuratorFramework zookeeperClient;

    private static final String keyPrefix = "redis:red:packet:"; //存储在缓存中的红包前缀key


    /**
     * 发红包业务，需要调用RedDetailService的服务完成
     * @param dto
     * @return
     * @throws Exception
     */
    @Override
    public String handOut(RedPackageDto dto) throws Exception {
        //首先判断合法性
        if(dto.getTotal() > 0 && dto.getAmount() > 0) {
            //二倍均值生成List
            List<Integer> redList = RedPacketUtil.divideRedPackage(dto.getAmount(),dto.getTotal());
            //生成标识的字符串.当前时间戳nanoTime
            String timeStamp = String.valueOf((System.nanoTime()));
            //生成记录随机金额的Key, 由前缀  发布用户ID和时间戳组成
            String redId = new StringBuffer(keyPrefix).append(dto.getUserId()).append(":").append(timeStamp).toString();
            //将随机金额的列表记录进入缓存，代表一个个红包, 使用List作为数据结构
            redisTemplate.opsForList().leftPushAll(redId,redList);
            //将红包的总数记录进入Cache
            String redTotalKey = redId + ":total";
            redisTemplate.opsForValue().set(redTotalKey,dto.getTotal());
            //异步将红包发出的红包和拆分的小红包分别记录进入数据库表中
            redDetailService.recordRedPacket(dto,redId,redList);
            //返回发布红包的唯一标识
            return redId;
        } else {
            throw new Exception("系统异常 分发红包：参数不合法");
        }
    }

    /**
     * 抢红包核心业务，需要首先在缓存中查找红包并且进行记录，抢到红包后需要调用DetailService写入数据库
     * @param userId
     * @param redId
     * @return
     * @throws Exception
     */
    @Override
    public BigDecimal rob(Integer userId, String redId) throws Exception {
        //Redis操作组件
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //抢红包前先判断是否抢过红包,如果已经抢过了，就直接返回红包金额，前台显示即可 redId : userID : rob  这里是抢到的ID
        Object object = valueOperations.get(redId + ":" + userId + ":rob");
        if(object != null) {
            return new BigDecimal(object.toString());
        }
        //点红包 --- 判断缓存系统中是否有红包剩余，> 0
        Boolean res = clik(redId);
        //有红包则进入拆红包业务逻辑
        if(res) {
            //分布式锁，每一个人一次只能抢到一次红包，一对一关系
//            final String lockKey = redId + ":" + userId + "-lock";
            final  String lockKey = "/middleware/zkLock/" + redId + "-" + userId + "-lock";
            InterProcessMutex mutex = new InterProcessMutex(zookeeperClient,lockKey);
            //从小红包列表中拆一个红包

            //调用setIfAbsent方法，实现分布式锁，也就是这里pop是只能一次弹一个
//            Boolean lock = valueOperations.setIfAbsent(lockKey,redId);
            //设置分布式锁过期时间24小时
//            redisTemplate.expire(lockKey,24L,TimeUnit.HOURS);

            try {
                //判断当前线程是否获取到了锁
//                if(lock) {
                //这里采用zookeeper获取锁
                if(mutex.acquire(10L,TimeUnit.SECONDS)) {
                    Object value = redisTemplate.opsForList().rightPop(redId);
                    //如果红包金额不为空，说明有红包，有钱
                    if(value != null) {
                        //抢到一个，总数减少
                        String redTotalKey = redId + ":total";
                        //获取当前总数，如果为空则总数为0， 同时更新; 但这里的操作不是但命令，不具有原子性，会出现安全问题，需要修改
//                        Integer currentTotal = valueOperations.get(redTotalKey) != null ? (Integer)valueOperations.get(redTotalKey) : 0;
//                        valueOperations.set(redTotalKey,currentTotal - 1);
                        valueOperations.increment(redTotalKey,-1);

                        //处理红包金额为元
                        BigDecimal result = new BigDecimal(value.toString()).divide(new BigDecimal(100));
                        //抢到红包的信息记录进入数据库
                        redDetailService.recordRobedPacket(userId,redId,new BigDecimal(value.toString()));
                        //当前用户抢过红包了，使用Key进行标识，设置过期时间为1天
                        valueOperations.set(redId + ":" + userId + ":rob",result,24L, TimeUnit.HOURS);
                        log.info("当前用户抢到红包了：剩余红包数量：{},userId={} key={} 金额={}",valueOperations.get(redTotalKey),userId,redId,result);

                        return result;
                    }
                }
            } catch (Exception e) {
                throw new Exception("加分布式锁失败");
            } finally {
                //分布式锁必须释放
//                if(Objects.equals(redId,valueOperations.get(lockKey))) {
//                    //释放当前线程的redis的锁key
//                    redisTemplate.delete(lockKey);
//                }
                mutex.release();
            }
        }
        //null表示当前用户没有抢到红包
        return null;
    }

    /**
     * 点红包业务逻辑： 判断缓存系统是否有红包
     */
    private Boolean clik(String redId) throws Exception {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //获取红包总数 key:total
        String redTotalKey = redId + ":total";
        //获取剩余个数
        Object total = valueOperations.get(redTotalKey);
        //判断剩余个数是否大于0
        if(total != null && Integer.valueOf(total.toString()) > 0) {
            //还有红包
            return true;
        }
        return false;
    }
}
